package net.edwardstx.brian.rpn;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.edwardstx.brian.rpn.RPNStackItem.Type;
import net.edwardstx.brian.rpn.exception.InvalidArguments;
import net.edwardstx.brian.rpn.exception.OperationExecutionException;
import net.edwardstx.brian.rpn.exception.UnknownOperation;
import net.edwardstx.brian.rpn.type.annotations.RPNOperation;
import net.edwardstx.brian.rpn.type.annotations.RPNOperationArguments;
import net.edwardstx.brian.rpn.types.RPNInteger;


public class RPNEngineImpl implements RPNEngine {
	private static final Logger logger = LoggerFactory.getLogger(RPNEngineImpl.class);
	private LinkedList<RPNStackItem> stack = new LinkedList<>();
	private SortedSet<Type> registeredTypes = new TreeSet<>();
	private Map<String, Set<Method>> methods = new TreeMap<>();
	private List<Method> parsers = new LinkedList<>();
	
	
	public RPNEngineImpl(){
		registerMethods(RPNEngineImpl.class);
	}
	
	public void register(Type type){
		if(registeredTypes.contains(type))
			return;
		try {
			Method method = type.getTypeClass().getMethod("parse", String.class);
			if(Modifier.isStatic(method.getModifiers()) && RPNStackItem.class.isAssignableFrom(method.getReturnType())){
				logger.info("Adding parse function for {}",type.getTypeName());
				parsers.add(method);
			}
		} catch (NoSuchMethodException | SecurityException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		logger.info("Registering {}",type.getTypeName());
		registerMethods(type.getTypeClass());
		registeredTypes.add(type);
	}
	
	private void registerMethods(Class clazz){
		for(Method method : clazz.getMethods()){
			logger.info("Checking method {}.{}",clazz.getName(),method.getName());
			RPNOperation operation = method.getAnnotation(RPNOperation.class);
			if(operation == null)
				continue;
			for(String key : operation.value()){
				logger.info("Adding {} op to {} for method {}",key, method.getName());
				addMethod(key.toUpperCase(), method);
			}
		}
		
	}
	
	private void addMethod(String key, Method method) {
		
		Set<Method> methodSet;
		if(methods.containsKey(key)){
			methodSet = methods.get(key);
		} else {
			methodSet = new HashSet<>();
			methods.put(key, methodSet);
		}
		methodSet.add(method);
	}

	@Override
	public RPNStackItem pop() {
		return stack.pop();
	}

	@Override
	public void dup() {
		stack.push(stack.peek());

	}

	@Override
	public void push(RPNStackItem item) {
		register(item.getAssociatedType());
		stack.push(item);

	}

	@Override
	public void execute(String command) throws UnknownOperation, InvalidArguments {
		if(methods.containsKey(command.toUpperCase())){
			Set<Method> methodSet = methods.get(command.toUpperCase());
			for(Method method : methodSet){
				RPNOperation operation = method.getAnnotation(RPNOperation.class);
				Deque<RPNStackItem> args = null;
				if( operation.arguments() == null ||  operation.arguments().length == 0){
					try {
						 args = bindArguments(method,method.getParameterTypes());
					} catch ( InvalidArguments e) {	}
				} else {
					for(RPNOperationArguments arguments : operation.arguments()){
						try{
							if(arguments.value().length <= stack.size())
							args = bindArguments(method,arguments.value());
						} catch (InvalidArguments e){}
						if(args != null)
							break;
					}
				}
				try {
					if(args != null){
						execute(method,args);
					return;
					}
				} catch (OperationExecutionException e) {
					
				}
			}
		} else {
			throw new UnknownOperation(command);
		}
	}
	
	private Deque<RPNStackItem> bindArguments(Method method, Class [] arguments) throws InvalidArguments{
		int i = 0;
		Deque<RPNStackItem> args = new ArrayDeque<>();
		for(Class<? extends RPNStackItem> clazz : arguments){
			RPNStackItem item = stack.get(i++);
			if(clazz.isInstance(item)){
				logger.debug("Adding arg {}: {}", i, item);
				args.add(item);
			}
			else
				throw new InvalidArguments();
		}
		return args;
	}
	
	
	
	private void execute(Method method, Deque<RPNStackItem> args) throws OperationExecutionException{
		if(RPNStackItem.class.isAssignableFrom(method.getReturnType())){
			try {
				
				RPNStackItem stackItem = (RPNStackItem) method.invoke(args.peek(), args.toArray());
				for(int i = 0; i < args.size(); i++)
					stack.pop();
				stack.push(stackItem);
			} catch (IllegalAccessException | IllegalArgumentException
					| InvocationTargetException e) {
				throw new OperationExecutionException(e);
			}
		}else if(Queue.class.isAssignableFrom(method.getReturnType())){
			try {
				Queue<RPNStackItem> ret =  (Queue<RPNStackItem>) method.invoke(args.peek(), args.toArray());
				for(int i = 0; i < args.size(); i++)
					stack.pop();
				while(!ret.isEmpty())
					stack.push(ret.remove());
			} catch (IllegalAccessException | IllegalArgumentException 
					| InvocationTargetException e) {
				// TODO Auto-generated catch block
				throw new OperationExecutionException(e);
			}

		}
		
		
	}
	
	public void input(String in) throws UnknownOperation, InvalidArguments{
		for(Method method : parsers){
			try{
				RPNStackItem item = (RPNStackItem) method.invoke(null, in);
				if(item != null){
					push(item);
					return;
				}
			} catch (Exception e){	
//				e.printStackTrace(System.err);
			}
		}
		execute(in);
	}

	@Override
	public RPNStackItem get(int i) {
		// TODO Auto-generated method stub
		return stack.get(i);
	}

	@Override
	public int depth() {
		return stack.size();
	}

	@Override
	public void clear() {
		stack.clear();		
	}
	
	@RPNOperation(value="DROP") //,arguments={@RPNOperationArguments(value={RPNStackItem.class})})
	public static Queue<RPNStackItem> drop(RPNStackItem l_0){
		Queue<RPNStackItem> duplicate = new ArrayDeque<>(0);
		return duplicate;
	}
	
	@RPNOperation(value="DUP") //,arguments={@RPNOperationArguments(value={RPNStackItem.class})})
	public static Queue<RPNStackItem> dup(RPNStackItem l_0){
		Queue<RPNStackItem> duplicate = new ArrayDeque<>(2);
		duplicate.add(l_0);
		duplicate.add(l_0);
		return duplicate;
	}
	
	@RPNOperation(value="DUP2") //,arguments={@RPNOperationArguments(value={RPNStackItem.class})})
	public static Queue<RPNStackItem> dup2(RPNStackItem l_0, RPNStackItem l_1){
		Queue<RPNStackItem> duplicate = new ArrayDeque<>(4);
		duplicate.add(l_1);
		duplicate.add(l_0);
		duplicate.add(l_1);
		duplicate.add(l_0);
		return duplicate;
	}
	@RPNOperation(value="SWAP") //,arguments={@RPNOperationArguments(value={RPNStackItem.class,RPNStackItem.class})})
	public static Queue<RPNStackItem> swap(RPNStackItem l_0,RPNStackItem l_1){
		Queue<RPNStackItem> duplicate = new ArrayDeque<>(2);
		duplicate.add(l_0);
		duplicate.add(l_1);
		return duplicate;
	}

	@RPNOperation(value="OVER") //,arguments={@RPNOperationArguments(value={RPNStackItem.class,RPNStackItem.class})})
	public static Queue<RPNStackItem> over(RPNStackItem l_0,RPNStackItem l_1){
		Queue<RPNStackItem> duplicate = new ArrayDeque<>(2);
		duplicate.add(l_1);
		duplicate.add(l_0);
		duplicate.add(l_1);
		return duplicate;
	}
}
